package com.gitee.wsl.ext.db.query

fun List<OrderBy<*>>.toSqlQueries(): List<SqlQuery> {
    if (isEmpty()) return emptyList()
    return mapIndexed { index, orderBy ->
        val treeName = "order_$index"
        SqlQuery(
            from = "json_tree(entity.value, '$') as $treeName",
            where = "$treeName.fullkey LIKE ?",
            parameters = 1,
            bindArgs = { bindString(orderBy.path) },
            orderBy = "$treeName.value ${orderBy.direction?.value ?: ""}",
        )
    }
}

data class SqlQuery(
    val from: String? = null,
    val where: String? = null,
    val parameters: Int = 0,
    val bindArgs: AutoIncrementSqlPreparedStatement.() -> Unit = {},
    val orderBy: String? = null,
) {
    constructor(left: SqlQuery, right: SqlQuery, operator: String) : this(
        from = listOfNotNull(left.from, right.from).joinToString(", "),
        where = "(${left.where} $operator ${right.where})",
        parameters = left.parameters + right.parameters,
        bindArgs = {
            left.bindArgs(this)
            right.bindArgs(this)
        }
    )

    fun identifier(): Int {
        var result = from?.hashCode() ?: 0
        result = 31 * result + (where?.hashCode() ?: 0)
        result = 31 * result + (orderBy?.hashCode() ?: 0)
        return result
    }
}

internal fun List<SqlQuery>.buildFrom(prefix: String = ", ") = mapNotNull { it.from }
    .joinToString(", ") { it }
    .let { if (it.isNotBlank()) "$prefix$it" else "" }

internal fun List<SqlQuery>.buildWhere(prefix: String = "WHERE") = mapNotNull { it.where }
    .joinToString(" AND ") { it }
    .let { if (it.isNotBlank()) "$prefix $it" else "" }

internal fun List<SqlQuery>.buildOrderBy(prefix: String = "ORDER BY") = mapNotNull { it.orderBy }
    .joinToString(", ") { it }
    .let { if (it.isNotBlank()) "$prefix $it" else "" }


internal fun List<SqlQuery>.sumParameters(): Int = sumOf { it.parameters }

internal fun List<SqlQuery>.identifier(): Int = fold(0) { acc, sqlQuery ->
    31 * acc + sqlQuery.identifier()
}

class AutoIncrementSqlPreparedStatement(
    private var index: Int = 0,
    private val preparedStatement: SqlPreparedStatement,
) {
    fun bindBoolean(boolean: Boolean?) {
        preparedStatement.bindBoolean(index, boolean)
        index++
    }

    fun bindBytes(bytes: ByteArray?) {
        preparedStatement.bindBytes(index, bytes)
        index++
    }

    fun bindDouble(double: Double?) {
        preparedStatement.bindDouble(index, double)
        index++
    }

    fun bindLong(long: Long?) {
        preparedStatement.bindLong(index, long)
        index++
    }

    fun bindString(string: String?) {
        preparedStatement.bindString(index, string)
        index++
    }

    fun <T> bindValue(value: T?) {
        when (value) {
            is Boolean -> bindBoolean(value)
            is ByteArray -> bindBytes(value)
            is Double -> bindDouble(value)
            is Number -> bindLong(value.toLong())
            is String -> bindString(value)
            is Enum<*> -> {
                // Doesn't support @SerialName for now https://github.com/Kotlin/kotlinx.serialization/issues/2956
//                val e = value as T
//                e::class.serializerOrNull()?.let {
//                    val sName = it.descriptor.getElementDescriptor(value.ordinal).serialName
//                    bindString(sName)
//                } ?: bindString(null)
                bindString(value.name) // use ordinal name for now (which is default serialization)
            }

            null -> bindString(null)
            else -> {
                // Compiler bug doesn't smart cast the value to non-null
                val v = requireNotNull(value) { "Unsupported value type: null" }
                throw IllegalArgumentException("Unsupported value type: ${v::class.simpleName}")
            }
        }
    }
}

interface SqlPreparedStatement{
    fun bindBoolean(index: Int, boolean: Boolean?)
    fun bindBytes(index: Int, bytes: ByteArray?)

    fun bindDouble(index: Int, bytes: Double?)

    fun bindLong(index: Int, bytes: Long?)

    fun bindString(index: Int, bytes: String?)

}
